<template>
  <div class="ms-frame" :style="frameSize">
    <div ref="view"
         :style="{ width: this.viewWidth, height: this.viewHeight }"
         class="ms-view"
         v-resize="resizeDom"
         @wheel="scrolling">

      <div class="ms-content" ref="content"
           :style="contentOffset" v-resize="resizeDom">
        <slot></slot>
      </div>
    </div>

    <div ref="verticalScroll"
         :style="verticalScrollStyle"
         class="ms-scroll"
         @mousedown="startDragging"
         @mouseenter="activateScroll(0)"
         @mouseleave="leaveScroll">
    </div>

    <div ref="horizontalScroll"
         :style="horizontalScrollStyle"
         class="ms-scroll"
         @mousedown="startDragging"
         @mouseenter="activateScroll(1)"
         @mouseleave="leaveScroll">
    </div>
  </div>
</template>

<script>
export default {
  name: "MyScroll",

  props: {
    width: {
      type: [String, Number],
      default: '100%'
    },
    height: {
      type: [String, Number],
      default: '100%'
    },
    scrollColor: {
      type: String,
      default: '#c1c1c1'
    },
    weight: {
      type: [String, Number],
      default: '10px'
    },
    scrollOpacity: {
      type: Number,
      default: 0.5
    },
    scrollSpeed: {
      type: Number,
      default: 1.0
    },
    cancelBubble: {
      type: Boolean,
      default: true,
    },
    rightPadding: {
      type: Number,
      default: 0
    },
    bottomPadding: {
      type: Number,
      default: 0
    },
    contentStyle: {
      type: Object,
    }
  },

  data() {
    return {
      isDragging: false,
      isOnScroll: false,
      scrollNum: 0,
      startX: 0,
      startY: 0,
      top: 0,
      left: 0,
      viewRealWidth: 0,
      viewRealHeight: 0,
      contentWidth: 0,
      contentHeight: 0,
    }
  },

  computed: {
    viewWidth() {
      if (typeof this.width === 'number') return this.width + 'px'
      return this.width
    },
    viewHeight() {
      if (typeof this.height === 'number') return this.height + 'px'
      return this.height
    },
    scrollWeight() {
      if (typeof this.weight === 'number') return this.weight + 'px'
      return this.weight
    },

    scrollHeight() {
      if (this.viewRealHeight >= this.contentHeight) return '0'
      const viewHeight = this.viewRealHeight + this.bottomPadding;
      return Math.max(viewHeight / this.contentHeight * viewHeight, 5) + 'px'
    },
    scrollWidth() {
      if (this.viewRealWidth >= this.contentWidth) return '0'
      // 避免横向滚动条与纵向重合，裁剪部分长度
      const viewWidth = this.viewRealWidth - parseInt(this.scrollWeight, 10) - 5 + this.rightPadding;
      return Math.max(viewWidth / this.contentWidth * viewWidth, 5) + 'px'
    },

    verticalScrollStyle() {
      return {
        width: this.scrollWeight,
        height: this.scrollHeight,
        borderRadius: parseInt(this.scrollWeight, 10) / 2 + 'px',
        backgroundColor: this.scrollColor,
        opacity: this.scrollOpacity.toString(),
        right: '0',
        top: '0',
        transform: `translateY(${this.top}px)`
      }
    },
    horizontalScrollStyle() {
      return {
        height: this.scrollWeight,
        width: this.scrollWidth,
        borderRadius: parseInt(this.scrollWeight, 10) / 2 + 'px',
        backgroundColor: this.scrollColor,
        opacity: this.scrollOpacity.toString(),
        bottom: '0',
        left: '0',
        transform: `translateX(${this.left}px)`
      }
    },

    maxScrollX() {
      if (this.scrollWidth === '0') return 0
      return this.viewRealWidth - parseInt(this.scrollWidth, 10)
          - parseInt(this.scrollWeight, 10) - 5 + this.rightPadding;
    },
    maxScrollY() {
      if (this.scrollHeight === '0') return 0
      return this.viewRealHeight - parseInt(this.scrollHeight, 10) + this.bottomPadding;
    },
    maxOffsetX() {
      return Math.max(this.contentWidth - this.viewRealWidth, 0)
    },
    maxOffsetY() {
      return Math.max(this.contentHeight - this.viewRealHeight, 0)
    },

    contentOffset() {
      return {
        transform: `translate(${-(this.left / (this.maxScrollX || 1) * this.maxOffsetX)}px,
        ${-(this.top / (this.maxScrollY || 1) * this.maxOffsetY)}px)`,
        ...this.contentStyle
      }
    },

    frameSize() {
      return {
        width: this.viewRealWidth ? this.viewRealWidth + this.rightPadding + 'px' : '100%',
        height: this.viewRealHeight ? this.viewRealHeight  + this.bottomPadding + 'px' : '100%'
      }
    }
  },

  methods: {
    activateScroll(num) {
      if (num === 0) {
        this.$refs.verticalScroll.style.opacity = '1.0';
      } else {
        this.$refs.horizontalScroll.style.opacity = '1.0';
      }
      this.scrollNum = num;
      this.isOnScroll = true;
    },

    deactivateScroll() {
      if (this.scrollNum === 0) this.$refs.verticalScroll.style.opacity = this.scrollOpacity.toString();
      else this.$refs.horizontalScroll.style.opacity = this.scrollOpacity.toString();
    },

    leaveScroll() {
      this.isOnScroll = false;
      if (!this.isDragging) this.deactivateScroll()
    },

    startDragging(e) {
      this.isDragging = true;
      this.startX = e.pageX;
      this.startY = e.pageY;
    },

    stopDragging() {
      this.isDragging = false;
      if (!this.isOnScroll) this.deactivateScroll();
    },

    emitScrollEvent() {
      const state = {
        clientWidth: this.viewRealWidth,
        clientHeight: this.viewRealHeight,
        extentWidth: this.$refs.content.scrollWidth,
        extentHeight: this.$refs.content.scrollHeight,
        scrollTop: -parseInt(this.contentOffset['top'], 10),
        scrollLeft: -parseInt(this.contentOffset['left'], 10),
      }
      this.$emit('scroll', state)
    },

    draggingScroll(e) {
      e.preventDefault();
      if (!this.isDragging) return
      if (this.scrollNum === 0) {
        const top = this.top + e.pageY - this.startY;
        this.top = Math.min(this.maxScrollY, Math.max(0, top));
        this.startY = e.pageY;
      } else {
        const left = this.left + e.pageX - this.startX;
        this.left = Math.min(this.maxScrollX, Math.max(0, left));
        this.startX = e.pageX;
      }
      this.emitScrollEvent();
    },

    scrolling(e) {
      if (this.cancelBubble) {
        e.stopPropagation();
        // 防止触发外部原生滚动条
        e.preventDefault();
      }
      if (this.isDragging) return
      this.top = Math.min(this.maxScrollY, Math.max(0, this.top + e.deltaY / 10 * this.scrollSpeed));
      this.activateScroll(0);
      setTimeout(() => this.deactivateScroll(0), 300);
      this.emitScrollEvent();
    },

    resizeDom(target, width, height) {
      if (target === 'view') {
        this.viewRealWidth = width;
        this.viewRealHeight = height;
      } else {
        this.contentWidth = width;
        this.contentHeight = height;
      }
    }
  },

  directives:{
    resize: {
      componentUpdated(el, binding) {
        if (el.classList.contains('ms-view')) {
          binding.value('view', el.offsetWidth, el.offsetHeight);
        } else {
          binding.value('content', el.scrollWidth, el.scrollHeight)
        }
      }
    }
  },

  mounted() {
    this.viewRealWidth = this.$refs.view.offsetWidth;
    this.viewRealHeight = this.$refs.view.offsetHeight;
    this.contentWidth = this.$refs.content.scrollWidth;
    this.contentHeight = this.$refs.content.scrollHeight;

    document.defaultView.addEventListener('mousemove', this.draggingScroll);
    document.defaultView.addEventListener('mouseup', this.stopDragging);
  },

  beforeDestroy() {
    document.defaultView.removeEventListener('mousemove', this.draggingScroll);
    document.defaultView.removeEventListener('mouseup', this.stopDragging);
  }
}
</script>

<style scoped>
  .ms-view {
    overflow: hidden;
  }

  .ms-content {
    position: relative;
  }

  .ms-scroll {
    position: absolute;
    transition: opacity 0.5s ease-out;
  }

  .ms-frame {
    position: relative;
  }
</style>